#
# Copyright (c) 2021, 2023 supercell
#
# SPDX-License-Identifier: BSD-3-Clause
#

module Luce
  # Maintains the context needed to parse a Markdown document
  class Document
    getter link_references = Hash(String, LinkReference).new

    # Footnote ref count, keys are case-sensitive and added by define syntax.
    getter footnote_references = Hash(String, Int32).new

    # Footnotes label by appearing order.
    #
    # They are case-sensitive and added by ref syntax.
    getter footnote_labels = [] of String

    getter link_resolver : Resolver?
    getter image_link_resolver : Resolver?

    @[Deprecated("Use `#encode_html?` instead. Will be removed in version 1.0.")]
    def encode_html : Bool
      encode_html?
    end

    getter? encode_html : Bool

    # Whether to use default block syntaxes
    @[Deprecated("Use `#with_default_block_syntaxes?` instead. Will be removed in version 1.0.")]
    def with_default_block_syntaxes : Bool
      with_default_block_syntaxes?
    end

    # Whether to use default block syntaxes
    getter? with_default_block_syntaxes : Bool

    # Whether to use default inline syntaxes.
    #
    # Need to set both `with_default_block_syntaxes?` and `encode_html?` to
    # `false` to disable all inline syntaxes including HTML encoding syntaxes.
    @[Deprecated("Use `#with_default_inline_syntaxes?` instead. Will be removed in version 1.0")]
    def with_default_inline_syntaxes : Bool
      with_default_inline_syntaxes?
    end

    # Whether to use default inline syntaxes.
    #
    # Need to set both `#with_default_block_syntaxes?` and `#encode_html?` to
    # `false` to disable all inline syntaxes including HTML encoding syntaxes.
    getter? with_default_inline_syntaxes : Bool

    getter block_syntaxes : Array(BlockSyntax) = Array(BlockSyntax).new
    getter inline_syntaxes : Array(InlineSyntax) = Array(InlineSyntax).new

    @[Deprecated("Use `#has_custom_inline_syntaxes?` instead. Will be removed in version 1.0")]
    def has_custom_inline_syntaxes : Bool
      has_custom_inline_syntaxes?
    end

    getter? has_custom_inline_syntaxes : Bool

    def initialize(
      block_syntaxes : Array(BlockSyntax)?,
      inline_syntaxes : Array(InlineSyntax)?,
      extension_set : ExtensionSet?,
      @link_resolver : Resolver? = nil,
      @image_link_resolver : Resolver? = nil,
      @encode_html : Bool = true,
      @with_default_block_syntaxes = true,
      @with_default_inline_syntaxes = true
    )
      @has_custom_inline_syntaxes = ((!inline_syntaxes.nil? && !inline_syntaxes.empty?) || false) ||
                                    ((!extension_set.nil? && !extension_set.inline_syntaxes.empty?) || false)

      @block_syntaxes.concat(block_syntaxes) unless block_syntaxes.nil?
      @inline_syntaxes.concat(inline_syntaxes) unless inline_syntaxes.nil?

      if extension_set.nil?
        if with_default_block_syntaxes
          @block_syntaxes.concat(ExtensionSet::COMMON_MARK.block_syntaxes)
        end

        if with_default_inline_syntaxes
          @inline_syntaxes.concat(ExtensionSet::COMMON_MARK.inline_syntaxes)
        end
      else
        @block_syntaxes.concat(extension_set.block_syntaxes)
        @inline_syntaxes.concat(extension_set.inline_syntaxes)
      end
    end

    # Parses the given *lines* of Markdown to a series of AST nodes.
    def parse_lines(lines : Array(String)) : Array(Node)
      parse_line_list(lines.map { |e| Line.new(e) })
    end

    # Parses the given *text* to a series of AST nodes.
    def parse(text : String) : Array(Node)
      lines = text.lines.map { |line| Line.new(line) }
      parse_line_list(lines)
    end

    # Parses the given *lines* of `Line` to a series of AST nodes.
    def parse_line_list(lines : Array(Line)) : Array(Node)
      nodes = BlockParser.new(lines, self).parse_lines
      parse_inline_content nodes
      # Do filter after parsing inline as we need ref count
      filter_footnotes(nodes)
    end

    # Parses the given inline Markdown *text* to a series of AST nodes.
    def parse_inline(text : String) : Array(Node)
      InlineParser.new(text, self).parse
    end

    private def parse_inline_content(nodes : Array(Node)) : Nil
      i = 0
      while i < nodes.size
        node = nodes[i]
        if node.is_a? UnparsedContent
          inline_nodes = parse_inline(node.text_content)
          nodes.delete_at(i)
          Luce.array_insert_all(nodes, i, inline_nodes)
          i += inline_nodes.size - 1
        elsif node.is_a?(Element) && !node.children.nil?
          # ameba:disable Lint/NotNil
          parse_inline_content(node.children.not_nil!)
        end
        i += 1
      end
    end

    # Footnotes could be defined in arbitrary positions of a document, we need
    # to distinguish them and put them behind; and every footnote definition
    # may have multiple backrefs, we need to append backrefs for it.
    private def filter_footnotes(nodes : Array(Node)) : Array(Node)
      footnotes = [] of Element
      blocks = [] of Node
      nodes.each do |node|
        if node.is_a?(Element) && node.tag == "li" && footnote_references.has_key?(node.footnote_label)
          label = node.footnote_label
          if (!label.nil?) && (count = footnote_references[label]) > 0
            footnotes << node
            children = node.children
            if !children.nil?
              append_backref(children, DartURI.encode_component(label), count || 0)
            end
          end
        else
          blocks << node
        end
      end

      unless footnotes.empty?
        ordinal = Hash(String, Int32).new
        (0...footnote_labels.size).each do |i|
          ordinal["fn-#{footnote_labels[i]}"] = i
        end
        footnotes.sort! do |lhs, rhs|
          idl = lhs.attributes["id"]? ? lhs.attributes["id"].to_s.downcase : ""
          idr = rhs.attributes["id"]? ? rhs.attributes["id"].to_s.downcase : ""
          (ordinal[idl]? || 0) - (ordinal[idr]? || 0)
        end
        list = Element.new("ol", footnotes + [] of Node)
        section = Element.new("section", [list] of Node)
        section.attributes["class"] = "footnotes"
        blocks << section
      end
      blocks
    end

    # Generate backref nodes, append them to footnote definition's last child.
    private def append_backref(children : Array(Node), ref : String, count : Int32) : Nil
      refs = [] of Node
      (0...count).each do |i|
        refs << Text.new(" ")
        refs << ElementExt.footnote_anchor(ref, i)
      end

      if children.empty?
        children.concat(refs)
      else
        last = children.last
        if last.is_a?(Element) && !last.children.nil?
          # ameba:disable Lint/NotNil
          last.children.not_nil!.concat(refs)
        else
          children << Element.new("p", [last] + refs)
        end
      end
    end
  end

  private class ElementExt < Element
    def self.footnote_anchor(ref : String, i : Int32) : Element
      num = "#{i + 1}"
      suffix = i > 0 ? "-#{num}" : ""
      e = Element.empty("tag")
      e.tag
      ret : Element
      if i > 0
        sup = Element.new("sup", [Text.new(num)] of Node)
        sup.attributes["class"] = "footnote-ref"
        ret = Element.new("a", [
          Text.new("\u21a9"),
          sup,
        ])
      else
        ret = Element.new("a", [
          Text.new("\u21a9"),
        ] of Node)
      end
      # Ignore GFM's attributes:
      # <data-footnote-backref aria-label="Back to content">.
      ret.attributes["href"] = "#fnref-#{ref}#{suffix}"
      ret.attributes["class"] = "footnote-backref"
      ret
    end
  end

  # A [link reference
  # definition](https://spec.commonmark.org/0.30/#link-reference-definitions).
  class LinkReference
    # The [link label](https://spec.commonmark.org/0.30/#link-label).
    #
    # Temporarily, this class is also being used to represent the link
    # data for an inline link (the destination and title), but this
    # should change before the shard is released.
    getter label : String

    # The [link destination](https://spec.commonmark.org/0.30/#link-destination).
    getter destination : String

    # The [link title](https://spec.commonmark.org/0.30/#link-title).
    getter title : String?

    # Construct a new `LinkReference`, with all necessary fields.
    #
    # If the parsed link reference definition does not include a title,
    # use `nil` for the *title* parameter.
    def initialize(@label : String, @destination : String, @title : String?)
    end
  end
end
